developmentor - a developer services company				  
				      Essential .NET Security
   Modules

Tokens

Estimated time for completion: 60 minutes


Goals of this exercise:

  • Get hands on experience with a system that uses Kerberos directly (via SSPI)
  • See your token change as you obtain logins across various machines in the classroom network

Overview

A token is produced as the result of authentication, and it represents a snapshot of a user's security profile, with a complete list of groups and privileges. Generated by the local authority on the machine where the user is attempting to access resources, it contains local groups and privileges from this machine, domain local groups from the domain in which the machine resides, and global groups from the client's domain, and universal groups from various domains within the client's forest.

As you can see, gathering the information necessary to produce a token is a tricky business, and tokens for the same user may look very different depending on the machine and domain where they are produced. In this lab, we'll set up the simplest possible Kerberos-based distributed system, one in which the server authenticates the client and replies with a description of the client's token (groups and privileges) that this particular server sees for the client.


part 1- Simulating a typical authorization landscape

In a real system, there are usually several domains, each with different security policies. This means that group definitions and membership lists will vary dramatically from domain to domain. In the classroom, you'll likely only have a single domain, but we can model an interesting authorization landscape using local groups. So before you get started, add some local groups to your machine, and assign various students to those groups. Be creative, and try to ensure that you add each student to at least one of your local groups.

steps:

  1. To add a local group, bring up the computer management console (compmgmt.msc) with administrative privileges, and drill down into System Tools, Local Users and Groups.

  2. Right click the Groups folder and choose New Group. Add several groups. Give them meaningful names, funny names, whatever. You'll be considered very boring if you use "group1", "group2", though!

  3. To add a user to a group, double click the group and go for it. Remember to select the authority you want (domain or your local machine) before you start scrolling through the user list, and remember you don't need to pick from the list if you'd rather just type the names into the dialog, delmited by semicolons. Once again, please be sure to add each student in class to at least one of your groups.


part 2- Peruse the client and server

Here you'll examine a simple client/server system that uses sockets for communication, and authenticates using Kerberos via an interface known as SSPI.

steps:

  1. Open up the NetworkLogon solution. There are three projects here, one is a Managed C++ class called TokenInfo that makes the job of examining tokens easier (you might find it interesting to peruse if you have extra time, so you can see some Win32 APIs that are used to get information from access tokens). The two C# projects (the client and server) are where you'll be working. Open up the client source file (networkLogonClient.cs), and start by looking at the Main entry point, at the top of the class.

  2. Note that we've already added a reference to an SSPI wrapper assembly provided by the .NET Remoting team at Microsoft. This SSPI wrapper allows you to use Kerberos from the CLR.

  3. Recall that in Kerberos, we need to know who the server is running as (what principal, that is) so we can get a ticket that the server will be able to decrypt. This is why the client app takes two command line arguments, the host name and the server principal name. For example, say we had a server running on machine MAC12, and it was launched by a user known as PWS\Student12. If a client on another machine wanted to contact this particular server, she'd use the following command:

    networkLogonClient mac12 pws\student12
    

    The first argument tells us the host name so we can open a socket to that machine. The second argument tells us what account the server is running under, so we can get a ticket for that principal. A common mistake is to confuse the host name with the server principal name. If you're not feeling comfortable with the distinction, ask your instructor to go over it with you - this is a really important concept to understand.

  4. The client first connects to the server with a socket, then uses that socket to send a Kerberos ticket to the server, establishing her identity. This is the job of the authenticate function. Have a look at this function now. Note that it's basically just a loop that exchanges data with the server (its primary job is to send a Kerb ticket to the server) until both sides are happy with the exchange. Note that the code doesn't explicitly call any Kerberos APIs, but rather uses a couple classes: ClientCredential and ClientContext. These are the SSPI wrappers I alluded to earlier. SSPI hides you from the details of Kerb tickets and just gives you what you need to send the server to authenticate. After this loop is finished, if there's been no exceptions, we've successfully authenticated with the server. At this point we return the resulting context object, which holds our copy of the Kerberos session key. We'll use this later in the lab to protect the channel.

  5. After authentication, Main calls makeRequestOverRawChannel to ask the server for a report on the token that was produced on his end as a result of authentication. This will show you the groups in which the server thinks you are a member. Remember that this information comes from two places: the domain controller (which put your domain groups in your Kerb ticket), and the server's local authority (which looked at the domain groups and added its own local groups before constructing the token that the server received for you).

  6. Now have a look at the server source file (networkLogonServer.cs). The Main function here should look very similar to the client code. For each client that connects, the server runs through the authentication handshake, getting the client's Kerberos ticket. After this exchange, the server ends up with an SSPI context represented by the ServerContext class. This holds a reference to the server's copy of the session key and the client's token, which is produced by SSPI once the authentication handshake completes. Have a look at the authenticateCaller function, and you'll see that it looks very similar to the client's code. It's just another loop that ferries data back and forth between the client and server Kerberos providers. The details are hidden behind the SSPI interface. Ultimately this code receives the client's Kerb ticket + authenticator, validates it, and produces a token that tells the server who the client is and what her groups are.

    Note the extra code at the bottom of authenticateCaller. The server is setting up Thread.CurrentPrincipal based on the token produced from SSPI. This is a common pattern in .NET - some low-level plumbing authenticates the user, then tucks the user's security information away in Thread.CurrentPrincipal for use elsewhere in the program.

  7. Have a look at the handleRequestOverRawChannel function in the server. This receives the client's request, looks at the client's token (which we tucked away earlier into Thread.CurrentPrincipal), and sends a report back to the client on what her user ID, groups, and privileges look like on this particular server.

  8. Both client and server are already complete, so build them. Bring up a command prompt and run your server from the command line so you can see its output as clients connect to you. Let the people around you know that you're running and ready to accept requests.


part 3- Authenticate with various servers

Now that your server is running, it's time to test your client against other servers on the network.

steps:

  1. Test your client against various servers in the room that are running (if you've been moving fast, you might need to wait awhile until the servers are running). You'll need to know the server's machine name to connect to, and the account of the student logged into the server machine. As a reminder, here's how to tell your client to connect to a machine named MAC12 where a user named PWS\Student12 is running the server app:

    networkLogonClient mac12 pws\student12
    
  2. As you move from server to server around the network, note how the groups in your token change, and how some stay the same. Think about how things might be different if you were doing this experiment across different domains - some of the domain groups might even change (domain local groups in particular).


part 4- Ticket caching and authorization latency

Microsoft's Kerberos implementation has many benefits, but one of the drawbacks of ticket caching is authorization latency. This is because group SIDs from the domain controller are stored in tickets, and since a ticket for a particular server is typically cached in the user's logon session all day long, if the user's domain groups change during the day, the group SIDs in these ticket's become stale. This part of the lab demonstrates this phenomenon.

steps:

  1. Run your client against a server close to you (it's easiest to work with the person next to you during this part of the lab). This will ensure that you've already obtained (and cached) a server ticket for your partner. Now ask the instructor to add you to a new domain group. At the same time, have your partner add you to a new local group on his or her machine.

  2. Once these changes are made, run your client against the same server again, and see what's changed. Did both new groups appear in the token the server got for you this time? If not, do you understand what happened? Make sure you and your partner both understand what's up. Call over the instructor to explain if it's not absolutely clear.

  3. Run the ticket viewer to view your ticket cache. Use the menus to purge your ticket cache (this is similar to logging off and logging back on again, but it isn't nearly as annoying). Run your client against the same server a third time. Has anything changed? Why?


part 5- Adding MAC protection and encryption to the channel

It's just plain silly to go to all the trouble of authenticating using Kerberos without using the resulting session key to protect the communication channel. Protecting the channel ensures the server and client that the data traveling between them is actually coming from the peer they authenticated with in the first place, that it has not been replayed, and that it's not being tampered with. Encryption can help deter eavesdroppers as well.

SSPI has two functions that can be used to protect data using the session key: SignMessage and EncryptMessage. The first simply adds MAC protection, while the second adds both MAC protection and encryption. Your job now is to secure the channel.

steps:

  1. So there won't be any confusion, we'll run secure servers on a different port (4243 instead of 4242), so update your client and server code to refer to the new port. Do this by changing the constant definition for "port" at the top of the Client class.

  2. Have a look at the client and server functions that send and receive messages over a secure channel (makeRequestOverSecureChannel and handleRequestOverSecureChannel, respectively). These functions do the same thing their raw counterparts did, but they go one step further by protecting the messages with the session key. Now instead of sending strings back and forth, you're going to be sending length-prefixed blocks of ciphertext. So go back to Main and change the subroutine calls so you're using these new functions instead. Note that these "secure" functions need to be given an SSPI Context object (which holds the session key) so they can do their work. This is returned from the authentication function, so grab it and pass it to the handleRequestOverSecureChannel function. Be sure to make this change in both the client and the server.

  3. Start your server just like before, and let people know that you're now listening on port 4243. Once some other people also have servers running, try out your client with them. This is a very important step! You've added support for confidentiality and integrity to this simple messaging system; it's a whole lot more robust now than it used to be.

  4. To simulate an active attack against your system, try having the server flip a single bit in the ciphertext just before calling BinaryWriter.Write down at the bottom of handleRequestOverSecureChannel. See if the client discovers the problem, or if it just decrypts the data happily:

    // flip a single bit somewhere in the ciphertext
    ciphertext[ciphertext.Length - 100] ^= 1;
    

    This is the whole point of calculating and sending a MAC - to ensure that attackers cannot modify the payload without detection!


part 6- Understanding Thread.CurrentPrincipal

Thread.CurrentPrincipal is designed to help you keep track of your client's identity in a server application. Setting this value is not the same as impersonating, in case you've heard that term (we'll cover it later, don't worry).

steps:

  1. To help verify this, go into the server code and try to open a file after Thread.CurrentPrincipal has been set by the authentication function. Here's some code you can cut and paste to test whether you can open a particular file:

    string fname = @"c:\labs\test.txt";
    try {
        new FileStream(fname, FileMode.Open, FileAccess.Read, FileShare.ReadWrite).Close();
        Console.WriteLine("Successfully opened {0} for reading.", fname);
    }
    catch (Exception x) {
        Console.WriteLine("Failed to open {0}:\n{1}", fname, x.Message);
    }
    
  2. Edit the DACL on the file so that only your server should have permission to this file. Even after Thread.CurrentPrincipal is set to someone else's security context, you should find that this has no effect on how the operating system sees you - your server is still running with your credentials, as far as the OS is concerned. In other words, you should be able to open the file no matter who your client is, and no matter how Thread.CurrentPrincipal is set. When we talk about impersonation later, you'll see how to go one step further and make the operating system use your client's credentials to do its access checks, instead of yours.


part 7- Using PrincipalPermissionAttribute for authorization

Since the plumbing has been kind enough to set up Thread.CurrentPrincipal, we can take advantage of this and use attributes to control access to various classes based on roles.

steps:

  1. Write a static method in the server class and annotate it with a PrincipalPermission attribute. Here's an example to get you started (change the group to one that you've assigned students to, of course):

    [PrincipalPermission(SecurityAction.Demand, Role=@"MAC5\GroupXXX")]
    static void GroupXXXOnly() {
    	Console.WriteLine("GroupXXXOnly was called");
    }
    
  2. Call this function anytime after Thread.CurrentPrincipal has been set. Use a try/catch block to catch any exceptions, and print out the exception message if the call fails. Verify that only requests from students in the group will succeed here and others will fail.